본문으로 건너뛰기

Part 3: API Security Design

ItemContent
Document NamePart 3: API Security Design
Product NameDTA Wide Sleep Management Platform
Date2026-02-10
ScopePart 3 (Backend API)

1. API Security Overview​

Objective: Comply with OWASP API Top 10, secure RESTful API design

Security Principles:

  • Authentication/authorization mandatory (JWT Bearer Token)
  • Input validation (DTO + class-validator)
  • Rate Limiting (Redis-based)
  • Minimal error message information disclosure
  • Additional protection for admin endpoints

2. Authentication and Authorization​

2.1 JWT-Based Authentication​

AppToken Structure (RS256 Asymmetric Signing):

// AppToken Payload (Actual Implementation - app-token.guard.ts)
{
"appId": "app-uuid",
"deviceId": "device-uuid",
"jti": "token-unique-id",
"exp": 1707579000, // Expire after 30 minutes
"iat": 1707577200
}

Authentication Flow:

Token Revocation Policy:

Inactivity Detection Implementation Status:

ItemNotes
AccessToken TTL 30-minute expirationBased on JWT exp claim
RefreshToken TTL 14-day expirationBased on JWT exp claim, long-term inactivity

2.2 Role-Based Authorization (RBAC)​

Decorator Usage:

@Controller('sleep')
@UseGuards(AppTokenGuard, FlexibleAuthGuard)
export class SleepController {
@Get('/logs')
@Roles('patient', 'clinician', 'admin')
async getSleepLogs(@CurrentUser() user: TokenPayload) {
// Patient: own data only
// Clinician: assigned patients only
// Admin: all data
}

@Delete('/logs/:id')
@Roles('patient', 'admin')
async deleteSleepLog(@Param('id') id: string) {
// Patient: delete own data only
// Admin: delete all data
}
}

2.3 App Request Integrity and Authenticity Verification​

Currently Implemented Validation Layers:

Validation LayerImplementation StatusReference FileNotes
AppToken (RS256) App Authenticity Verification✅ Implementedapp-token.guard.ts, app-token.service.tsJWK-based RS256 signing + DB status check
Device ID SHA-256 Hash Verification✅ Implementeddevice-identifier.service.tsRequest deviceIdHash vs server calculated value comparison
PubSub/Webhook GCP OIDC JWT Verification✅ Implementedpubsub.guard.ts, webhook-validation.service.tsGCP Pub/Sub request authentication
HMAC-based Request Body Signature Verification❌ Not implemented-No additional signature beyond AppToken
Response Body HMAC/Signature Addition❌ Not implemented-No signature layer on responses

3. Input Validation​

3.1 DTO-Based Validation​

CreateSleepLogDto:

import { IsString, IsDateString, IsOptional, IsInt, Min, Max } from 'class-validator';

export class CreateSleepLogDto {
@IsDateString()
bedtime: string; // ISO 8601 format

@IsDateString()
wakeTime: string;

@IsOptional()
@IsInt()
@Min(0)
@Max(480) // Maximum 8 hours
sol?: number; // Sleep Onset Latency (minutes)

@IsOptional()
@IsString()
@MaxLength(500)
notes?: string;
}

ValidationPipe Configuration:

// main.ts
app.useGlobalPipes(
new ValidationPipe({
transform: true, // Auto type conversion
whitelist: true, // Remove non-DTO properties
forbidNonWhitelisted: true, // Error on extra properties
exceptionFactory: (errors) => {
// Custom error response
return new BadRequestException({
code: 'VALIDATION_ERROR',
message: 'Input validation failed',
details: errors.map(e => ({
property: e.property,
constraints: e.constraints
}))
});
}
})
);

3.2 SQL Injection Prevention​

Prisma ORM Parameterization:

// ✅ SAFE: Prisma parameterized query
async findByUserId(userId: string): Promise<SleepLog[]> {
return this.prisma.sleepLog.findMany({
where: { userId: userId } // Auto parameterized
});
}

3.3 XSS Prevention​

HTML Escaping:

import { sanitize } from 'class-sanitizer';

export class CreateNoteDto {
@IsString()
@MaxLength(1000)
@sanitize() // Remove HTML tags
content: string;
}

// Or manual escaping
function syntax(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}

4. Rate Limiting and Anti-Abuse​

4.1 Rate Limiting Policy​

Endpoint TypeLimitWindowOn Exceeded
Authentication (Login)5 req/minutePer user429 + 1 min wait
Data Retrieval (GET)100 req/minutePer user429 + retry guidance
Data Creation (POST)10 req/minutePer user429 + retry guidance
Admin API60 req/minutePer user429 + logging
Public API1000 req/minutePer IP429 + CAPTCHA

4.2 Redis-Based Rate Limiter​

Implementation (NestJS Interceptor):

@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
constructor(private readonly redis: RedisService) {}

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const userId = request.user?.userId || request.ip;
const key = `rate-limit:${request.path}:${userId}`;

const current = await this.redis.incr(key);
if (current === 1) {
await this.redis.expire(key, 60); // 1 min TTL
}

const limit = this.getLimit(request.path);
if (current > limit) {
throw new TooManyRequestsException(`Rate limit exceeded. Try again in 60 seconds.`);
}

return next.handle();
}
}

4.3 Brute Force Protection​

Login Failure Limit:

async login(email: string, password: string) {
const key = `login-attempts:${email}`;
const attempts = await this.redis.get(key);

if (attempts && parseInt(attempts) >= 5) {
throw new TooManyRequestsException('Too many login attempts. Try again in 15 minutes.');
}

// Password verification
const isValid = await this.verifyPassword(email, password);

if (!isValid) {
await this.redis.incr(key);
await this.redis.expire(key, 900); // 15 minutes
throw new UnauthorizedException('Invalid credentials');
}

// Reset counter on success
await this.redis.del(key);
return this.generateToken(user);
}

5. Error Message Policy​

5.1 Safe Error Response​

Production Environment:

// ✅ SAFE: Minimal information disclosure
{
"code": 2051,
"message": "INVALID_TOKEN",
"detail": "The token is invalid.",
"timestamp": "2026-02-10T12:00:00.000Z"
}

Global Exception Filter:

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();

let status = 500;
let code = 'INTERNAL_SERVER_ERROR';
let detail = 'A server error occurred.';

if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
code = exceptionResponse['code'] || exception.message;
detail = exceptionResponse['detail'] || exception.message;
}

// ❌ Absolutely never expose stack traces
// ❌ Absolutely never expose database connection info
// ❌ Absolutely never expose file paths

response.status(status).json({
code,
message: code,
detail,
timestamp: new Date().toISOString()
});

// Internal logging (Cloud Logging / LoggerService)
if (status >= 500) {
this.logger.error(exception);
}
}
}
ItemImplementation StatusNotes
Error message minimization (no stack trace exposure)✅ ImplementedGlobalExceptionFilter

5.2 HTTP Status Code Mapping​

ScenarioHTTP StatusResponse CodeMessage
Authentication failed (no token)401AUTH_REQUIRED"Authentication required."
Authentication failed (token expired)401TOKEN_EXPIRED"Token has expired."
Permission denied403FORBIDDEN"Permission denied."
Resource not found404NOT_FOUND"Resource not found."
Input validation failed400VALIDATION_ERROR"Input value is invalid."
Rate limit exceeded429RATE_LIMIT_EXCEEDED"Request limit exceeded."
Server error500INTERNAL_ERROR"A server error occurred."

6. Admin Endpoint Protection​

6.1 Additional Security Layer​

Admin-only Guard:

@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;

// 1. Check Admin role
if (!user.roles?.includes('admin')) {
return false;
}

// 2. IP whitelist check (optional)
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
if (!this.isAllowedIp(clientIp)) {
this.logger.warn(`Admin access from unauthorized IP: ${clientIp}`);
return false;
}

// 3. Time-based access restriction (optional)
const hour = new Date().getUTCHours();
if (hour >= 0 && hour < 6) {
this.logger.warn(`Admin access during restricted hours: ${user.userId}`);
// Warn only, don't block
}

return true;
}
}

6.2 Sensitive API Endpoint List​

EndpointRoleAdditional SecurityAudit Log
DELETE /v1/admin/users/:idAdminMFA reconfirmation✅
POST /v1/admin/users/:id/rolesAdminJira approval✅
GET /v1/admin/audit-logsAdminIP whitelist✅
POST /v1/admin/system/settingsAdminJira approval✅
GET /v1/admin/users/:id/data-exportAdmin, CSData anonymization✅

7. API Security Checklist​

#ItemImplementationValidation
1All endpoints require authentication (except public)✅API Test
2Role-based authorization (RBAC)✅API Test
3DTO input validation (class-validator)✅API Test
4SQL Injection prevention (Prisma ORM)✅SAST
5XSS prevention (HTML escaping)✅DAST
6CSRF prevention (SameSite Cookie)N/AJWT Bearer Token used
7Rate Limiting (Redis)✅API Test
8Safe error messages✅API Test
9HTTPS enforcement (TLS 1.3)✅GCP SSL
10Sensitive data logging prohibition✅Log sample validation
11CORS appropriate configuration✅Configuration review
12API versioning (v1/)✅URL pattern
13Admin API additional protection✅Admin Guard
14Audit logging (sensitive actions)✅Log sample

8. OWASP API Top 10 Compliance​

#OWASP API Top 10Implementation ControlStatus
1Broken Object Level AuthorizationuserId validation, RBAC✅
2Broken AuthenticationJWT, Rate Limiting✅
3Broken Object Property Level AuthorizationDTO Whitelist✅
4Unrestricted Resource ConsumptionRate Limiting, pagination✅
5Broken Function Level AuthorizationRoles Guard✅
6Unrestricted Access to Sensitive Business FlowsRate Limiting, block access✅
7Server Side Request Forgery (SSRF)URL validation, whitelist✅
8Security MisconfigurationSecure defaults, SAST✅
9Improper Inventory ManagementSwagger docs, API versioning✅
10Unsafe Consumption of APIsExternal API TLS validation, timeout✅

9. Swagger API Documentation Security​

9.1 Swagger UI Access Restriction​

Production Environment:

// main.ts
if (process.env.NODE_ENV !== 'production') {
// Enable Swagger only in staging/development
const config = new DocumentBuilder()
.setTitle('DTA Wide API')
.setVersion('1.0')
.addBearerAuth()
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('v1/docs', app, document);
} else {
// Production: Disable Swagger or IP whitelist
// [TODO: Production Swagger disabling verification needed]
}

9.2 Hide Sensitive Information​

@ApiProperty({
example: 'user@example.com',
description: 'User email'
// ❌ Never include: actual emails, tokens, passwords
})
email: string;

@ApiProperty({
example: '********', // Masked example
writeOnly: true, // Exclude from response
})
@Exclude() // Auto-exclude with class-transformer
password: string;

Evidence and References (Artifacts)​

  1. API Security Checklist (Section 7 of this document)
  2. DTO Validation Code - apps/dta-wide-api/src/app/*/dto/*.dto.ts
  3. Guards Implementation - guards/app-token.guard.ts, guards/flexible-auth.guard.ts, guards/webhook.guard.ts
  4. Rate Limiter Code - libs/core/redis/src/lib/rate-limit.interceptor.ts
  5. Exception Filter Code - apps/dta-wide-api/src/app/filters/global-exception.filter.ts
  6. OWASP ZAP DAST Report - reports/api-dast-latest.pdf
  7. Swagger API Documentation - https://staging-api.dta-wide.com/v1/docs (staging)
  8. API Security Test Results - test-results/api-security-tests.log
  9. Rate Limiting Test - k6 script + results
  10. Audit Log Sample (admin API) - logs/admin-api-calls.log
Test ItemToolResultNotes
SQL InjectionOWASP ZAP[TODO: Execution needed]Prisma ORM (parameterized)
XSS (Reflected)OWASP ZAP[TODO: Execution needed]HTML escaping
Broken AuthenticationBurp Suite[TODO: Execution needed]AppToken RS256 + Rate Limit
IDOR (Insecure Direct Object Reference)Manual Test[TODO: Execution needed]userId validation
Rate Limitingk6 Load Test[TODO: Execution needed]100 req/min per user
CSRFOWASP ZAPN/AJWT Bearer Token (no cookies)
Sensitive Information DisclosureManual Review[TODO: Execution needed]Error message minimization